{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4cd4f701",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/langchain-ai/langchain-academy/blob/main/module-4/map-reduce.ipynb) [![Open in LangChain Academy](https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/66e9eba12c7b7688aa3dbb5e_LCA-badge-green.svg)](https://academy.langchain.com/courses/take/intro-to-langgraph/lessons/58239947-lesson-3-map-reduce)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36737349-c949-4d64-9aa3-3767cbd02ad1",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "# Map-reduce\n",
    "\n",
    "## Review\n",
    "\n",
    "We're building up to a multi-agent research assistant that ties together all of the modules from this course.\n",
    "\n",
    "To build this multi-agent assistant, we've been introducing a few LangGraph controllability topics.\n",
    "\n",
    "We just covered parallelization and sub-graphs.\n",
    "\n",
    "## Goals\n",
    "\n",
    "Now, we're going to cover [map reduce](https://docs.langchain.com/oss/python/langgraph/use-graph-api#map-reduce-and-the-send-api)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f24e95c8",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install -U langchain_openai langgraph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff57cbf7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, getpass\n",
    "\n",
    "def _set_env(var: str):\n",
    "    if not os.environ.get(var):\n",
    "        os.environ[var] = getpass.getpass(f\"{var}: \")\n",
    "\n",
    "_set_env(\"OPENAI_API_KEY\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbcd868a",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "We'll use [LangSmith](https://docs.langchain.com/langsmith/home) for [tracing](https://docs.langchain.com/langsmith/observability-concepts)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9fdc647f",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "_set_env(\"LANGSMITH_API_KEY\")\n",
    "os.environ[\"LANGSMITH_TRACING\"] = \"true\"\n",
    "os.environ[\"LANGSMITH_PROJECT\"] = \"langchain-academy\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2bbe9b9f-4375-4bca-8e32-7d57cb861469",
   "metadata": {},
   "source": [
    "## Problem\n",
    "\n",
    "Map-reduce operations are essential for efficient task decomposition and parallel processing. \n",
    "\n",
    "It has two phases:\n",
    "\n",
    "(1) `Map` - Break a task into smaller sub-tasks, processing each sub-task in parallel.\n",
    "\n",
    "(2) `Reduce` - Aggregate the results across all of the completed, parallelized sub-tasks.\n",
    "\n",
    "Let's design a system that will do two things:\n",
    "\n",
    "(1) `Map` - Create a set of jokes about a topic.\n",
    "\n",
    "(2) `Reduce` - Pick the best joke from the list.\n",
    "\n",
    "We'll use an LLM to do the job generation and selection."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "994cf903-1ed6-4ae2-b32a-7891a2808f81",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# Prompts we will use\n",
    "subjects_prompt = \"\"\"Generate a list of 3 sub-topics that are all related to this overall topic: {topic}.\"\"\"\n",
    "joke_prompt = \"\"\"Generate a joke about {subject}\"\"\"\n",
    "best_joke_prompt = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one, starting 0 as the ID for the first joke. Jokes: \\n\\n  {jokes}\"\"\"\n",
    "\n",
    "# LLM\n",
    "model = ChatOpenAI(model=\"gpt-4o\", temperature=0) "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3b883cc-3469-4e96-b1a4-deadf7bf3ce5",
   "metadata": {},
   "source": [
    "## State\n",
    "\n",
    "### Parallelizing joke generation\n",
    "\n",
    "First, let's define the entry point of the graph that will:\n",
    "\n",
    "* Take a user input topic\n",
    "* Produce a list of joke topics from it\n",
    "* Send each joke topic to our above joke generation node\n",
    "\n",
    "Our state has a `jokes` key, which will accumulate jokes from parallelized joke generation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "099218ca-ee78-4291-95a1-87ee61382e3b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import operator\n",
    "from typing import Annotated\n",
    "from typing_extensions import TypedDict\n",
    "from pydantic import BaseModel\n",
    "\n",
    "class Subjects(BaseModel):\n",
    "    subjects: list[str]\n",
    "\n",
    "class BestJoke(BaseModel):\n",
    "    id: int\n",
    "    \n",
    "class OverallState(TypedDict):\n",
    "    topic: str\n",
    "    subjects: list\n",
    "    jokes: Annotated[list, operator.add]\n",
    "    best_selected_joke: str"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7176d1c-4a88-4b0f-a960-ee04a45279bd",
   "metadata": {},
   "source": [
    "Generate subjects for jokes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "45010efd-ad31-4daa-b77e-aaec79ef0309",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_topics(state: OverallState):\n",
    "    prompt = subjects_prompt.format(topic=state[\"topic\"])\n",
    "    response = model.with_structured_output(Subjects).invoke(prompt)\n",
    "    return {\"subjects\": response.subjects}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e5296bb0-c163-4e5c-8181-1e305b37442a",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Here is the magic: we use the  [Send](https://docs.langchain.com/oss/python/langgraph/graph-api/#send) to create a joke for each subject.\n",
    "\n",
    "This is very useful! It can automatically parallelize joke generation for any number of subjects.\n",
    "\n",
    "* `generate_joke`: the name of the node in the graph\n",
    "* `{\"subject\": s`}: the state to send\n",
    "\n",
    "`Send` allow you to pass any state that you want to `generate_joke`! It does not have to align with `OverallState`.\n",
    "\n",
    "In this case, `generate_joke` is using its own internal state, and we can populate this via `Send`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "bc83e575-11f6-41a9-990a-adb571bcda06",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from langgraph.types import Send\n",
    "def continue_to_jokes(state: OverallState):\n",
    "    return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9847192d-d358-411e-90c0-f06be0738717",
   "metadata": {},
   "source": [
    "### Joke generation (map)\n",
    "\n",
    "Now, we just define a node that will create our jokes, `generate_joke`!\n",
    "\n",
    "We write them back out to `jokes` in `OverallState`! \n",
    "\n",
    "This key has a reducer that will combine lists."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "bcddc567-73d3-4fb3-bfc5-1bea538f2aab",
   "metadata": {},
   "outputs": [],
   "source": [
    "class JokeState(TypedDict):\n",
    "    subject: str\n",
    "\n",
    "class Joke(BaseModel):\n",
    "    joke: str\n",
    "\n",
    "def generate_joke(state: JokeState):\n",
    "    prompt = joke_prompt.format(subject=state[\"subject\"])\n",
    "    response = model.with_structured_output(Joke).invoke(prompt)\n",
    "    return {\"jokes\": [response.joke]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02960657-d174-4076-99a8-b3f9eea015f4",
   "metadata": {},
   "source": [
    "### Best joke selection (reduce)\n",
    "\n",
    "Now, we add logic to pick the best joke."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "8d672870-75e3-4307-bda0-c41a86cbbaff",
   "metadata": {},
   "outputs": [],
   "source": [
    "def best_joke(state: OverallState):\n",
    "    jokes = \"\\n\\n\".join(state[\"jokes\"])\n",
    "    prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n",
    "    response = model.with_structured_output(BestJoke).invoke(prompt)\n",
    "    return {\"best_selected_joke\": state[\"jokes\"][response.id]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "837cd12e-5bff-426e-97f4-c774df998cfb",
   "metadata": {},
   "source": [
    "## Compile"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "2ae6be4b-144e-483c-88ad-ce86d6477a0d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from IPython.display import Image\n",
    "from langgraph.graph import END, StateGraph, START\n",
    "\n",
    "# Construct the graph: here we put everything together to construct our graph\n",
    "graph = StateGraph(OverallState)\n",
    "graph.add_node(\"generate_topics\", generate_topics)\n",
    "graph.add_node(\"generate_joke\", generate_joke)\n",
    "graph.add_node(\"best_joke\", best_joke)\n",
    "graph.add_edge(START, \"generate_topics\")\n",
    "graph.add_conditional_edges(\"generate_topics\", continue_to_jokes, [\"generate_joke\"])\n",
    "graph.add_edge(\"generate_joke\", \"best_joke\")\n",
    "graph.add_edge(\"best_joke\", END)\n",
    "\n",
    "# Compile the graph\n",
    "app = graph.compile()\n",
    "Image(app.get_graph().draw_mermaid_png())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "e21dc7c9-0add-4125-be76-af701adb874a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'generate_topics': {'subjects': ['mammals', 'reptiles', 'birds']}}\n",
      "{'generate_joke': {'jokes': [\"Why don't mammals ever get lost? Because they always follow their 'instincts'!\"]}}\n",
      "{'generate_joke': {'jokes': [\"Why don't alligators like fast food? Because they can't catch it!\"]}}\n",
      "{'generate_joke': {'jokes': [\"Why do birds fly south for the winter? Because it's too far to walk!\"]}}\n",
      "{'best_joke': {'best_selected_joke': \"Why don't alligators like fast food? Because they can't catch it!\"}}\n"
     ]
    }
   ],
   "source": [
    "# Call the graph: here we call it to generate a list of jokes\n",
    "for s in app.stream({\"topic\": \"animals\"}):\n",
    "    print(s)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "2a96517e-77ab-46e2-95e2-79168c044e9c",
   "metadata": {},
   "source": [
    "## Studio\n",
    "\n",
    "**⚠️ Notice**\n",
    "\n",
    "Since filming these videos, we've updated Studio so that it can now be run locally and accessed through your browser. This is the preferred way to run Studio instead of using the Desktop App shown in the video. It is now called _LangSmith Studio_ instead of _LangGraph Studio_. Detailed setup instructions are available in the \"Getting Setup\" guide at the start of the course. You can find a description of Studio [here](https://docs.langchain.com/langsmith/studio), and specific details for local deployment [here](https://docs.langchain.com/langsmith/quick-start-studio#local-development-server).  \n",
    "To start the local development server, run the following command in your terminal in the `/studio` directory in this module:\n",
    "\n",
    "```\n",
    "langgraph dev\n",
    "```\n",
    "\n",
    "You should see the following output:\n",
    "```\n",
    "- 🚀 API: http://127.0.0.1:2024\n",
    "- 🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024\n",
    "- 📚 API Docs: http://127.0.0.1:2024/docs\n",
    "```\n",
    "\n",
    "Open your browser and navigate to the **Studio UI** URL shown above.\n",
    "\n",
    "Let's load the above graph in the Studio UI, which uses `module-4/studio/map_reduce.py` set in `module-4/studio/langgraph.json`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "741a5e45-9a4c-43b4-8393-9298b3dcda53",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
